/* Copyright 2009 Requirement Management System
 * 
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.mags.dao.hibernate;

import java.io.InputStream;
import java.io.Serializable;
import java.sql.Blob;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.beanutils.PropertyUtils;
import org.hibernate.CacheMode;
import org.hibernate.Criteria;
import org.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.hibernate.LockMode;
import org.hibernate.Query;
import org.hibernate.ScrollMode;
import org.hibernate.ScrollableResults;
import org.hibernate.Session;
import org.hibernate.criterion.Conjunction;
import org.hibernate.criterion.CriteriaSpecification;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Disjunction;
import org.hibernate.criterion.Junction;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.ProjectionList;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Restrictions;
import org.hibernate.metadata.ClassMetadata;
import org.hibernate.persister.entity.Joinable;
import org.hibernate.persister.entity.SingleTableEntityPersister;
import org.hibernate.type.Type;
import org.springframework.orm.hibernate3.HibernateCallback;
import org.springframework.orm.hibernate3.SessionFactoryUtils;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
import org.mags.dao.common.Aggregate;
import org.mags.dao.common.RestrictionAttribute;
import org.mags.dao.common.BooleanOperator;
import org.mags.dao.common.DirectionOrder;
import org.mags.dao.common.EntityDefinition;
import org.mags.dao.common.EntityProperties;
import org.mags.dao.common.Limits;
import org.mags.dao.common.DataAccessException;
import org.mags.dao.common.MDAO;
import org.mags.dao.common.SOrder;
import org.mags.dao.common.RestrictionBuilderFactory;
import org.mags.dao.common.DAO;
import org.mags.dao.common.DBOperation;
import org.mags.dao.common.RestrictionBuilder;
import org.mags.dao.common.Transaction;
import org.mags.dao.common.Attribute;

/**
 * @author Mario Gait�n
 */

public class HibernateDao extends HibernateDaoSupport 
	implements DAO, MDAO, Serializable, RestrictionBuilderFactory {
	private static final long serialVersionUID = 1L;
	public static final String ID = "([a-zA-Z][\\w\\.]*)";
	public static final String ASSOCIATION_PATH_EXPRESSION = " *"+ID+"( +(as) +"+ID+")?( +(innerJoin|fullJoin|leftJoin))? *";
	public static final String PROJECTION_TYPE_PROPERTY = "property";
	public static final String PROJECTION_TYPE_DISTINCTPROPERTY = "distinctprop";
	public static final String PROJECTION_TYPE_SQL = "sql";
	public static final String PROJECTION_EXPRESSION = " *"+ID+"( +("+PROJECTION_TYPE_SQL+"|"+PROJECTION_TYPE_PROPERTY+"|"+PROJECTION_TYPE_DISTINCTPROPERTY+"))? *";
	private static final Collection<?> EMPTY_ARRAY = Collections.EMPTY_LIST;
	protected static final String ACTION_CREATEORUPDATE = "CREATEORUPDATE";
	protected static final String ACTION_CREATE = "CREATE";
	protected static final String ACTION_UPDATE = "UPDATE";
	protected static final String ACTION_DELETE = "DELETE";
	protected static final String ACTION_GETBYIDFORUPDATE = "GETBYIDANDLOCK";
	protected static final String ACTION_GETBYID = "GETBYID";
	protected static final String ACTION_EXECUTEUPDATE = "EXECUTEUPDATE";
	protected static final String ACTION_EXECUTEDELETE = "EXECUTEDELETE";
	protected static final String ACTION_EXECUTEQUERY = "EXECUTEQUERY";
	protected static final String ACTION_COUNT = "COUNT";
	protected static final String ACTION_GETSUM = "GETSUM";
	protected static final String ACTION_GETAVG = "GETAVG";
	protected static final String ACTION_GETMIN = "GETMIN";
	protected static final String ACTION_GETMAX = "GETMAX";
	protected static final String ACTION_GETBYRESTRICTIONASMAP = "GETBYRESTRICTIONASMAP";
	protected static final String ACTION_GETBYRESTRICTION = "GETBYRESTRICTION";
	private transient TransactionTemplate transactionTemplate = null;
	private static final long CACHE_SIZE = 50;
	
	private void clearCache() {
		super.getHibernateTemplate().flush();
		super.getHibernateTemplate().clear();
	}
	public HibernateDao() {
	}
	@Override
	public void createOrUpdate(Object record) {
		try {
			getHibernateTemplate().saveOrUpdate(record);
			clearCache();
		} catch(Exception e) {
			throw new DataAccessException(record.getClass(), ACTION_CREATEORUPDATE, e); 
		}
	}	
	@Override
	public Serializable create(Object record) {
		try {
			Serializable id= getHibernateTemplate().save(record);
			clearCache();
			return id;
		} catch(Exception e) {
			throw new DataAccessException(record.getClass(), ACTION_CREATE, e);			
		}
	}
	@Override
	public void update(Object record) {
		try {
			getHibernateTemplate().update(record);
			clearCache();
		} catch(Exception e) {
			throw new DataAccessException(record.getClass(), ACTION_UPDATE, e);
		}
	}
	@Override
	public void delete(Object record) {
		try {
			getHibernateTemplate().delete(record);
			clearCache();
		} catch(Exception e) {
			throw new DataAccessException(record.getClass(), ACTION_DELETE, e);			
		}
	}
	@Override
	public String getPropertyTypeFromMetaData(EntityDefinition entityDef, String property) {
		ClassMetadata cmd = getSessionFactory().getClassMetadata(entityDef.getEntityClass());
		if (cmd == null)
	        return null;
		Type type = cmd.getPropertyType(property);
		if(type==null)
			return null;
		return type.getName();
	}
	@Override
	public String[] getPropertiesFromMetaData(EntityDefinition entityDef) {
		ClassMetadata cmd = getSessionFactory().getClassMetadata(entityDef.getEntityClass());
		if (cmd == null)
	        return null;
		return cmd.getPropertyNames();
	}
	/*
	public String createSQLInsert(Object object) {
		try {
			SessionFactoryImplementor implementor = (SessionFactoryImplementor)getSessionFactory();
			Dialect dialect = implementor.getDialect();
			Insert insert = new Insert(dialect);
			EntityDefinition entityDef = new EntityDefinition(object.getClass());
			ClassMetadata cmd = getSessionFactory().getClassMetadata(object.getClass());
			if(cmd==null)
				throw new RuntimeException("class does not exists in hibernate configuration");
			String tableName = getTableFromMetaData(entityDef);
			insert.setTableName(tableName);
			SingleTableEntityPersister entity = findEntity(entityDef);
			Field[] fields = getFields(object);
			NullValue nullValue = new NullValue();
			for(Field field: fields) {
				String propertyName = field.getName();
				Type type = null;
				try {
					type = entity.getPropertyType(propertyName);
				} catch(Exception e) {
					continue;
				}
				System.out.println(type.getClass().getName());
				if(type instanceof LiteralType
				|| type instanceof ManyToOneType) {
					if(type instanceof ManyToOneType) {
						ManyToOneType manyType = (ManyToOneType)type;
						System.out.println(manyType.getPropertyName());
					}
					String[] columnNames = entity.getPropertyColumnNames(propertyName);
					System.out.println(columnNames);
					String columnName = null;
					if(columnNames.length>0)
						columnName = columnNames[0];
					if(columnName!=null) {
						field.setAccessible(true);
						Object value = field.get(object);
						insert.addColumn(columnName, value, value==null?nullValue:(LiteralType)type);
					}
				}
			}
			return insert.toStatementString();
		} catch(Exception e) {
			if(e instanceof RuntimeException)
				throw (RuntimeException)e;
			else
				throw new RuntimeException(e);
		}
	}*/
	@Override
	public String getColumnFromMetadata(EntityDefinition entityDef, String propertyName) {
		Map<?,?> metadata = getSessionFactory().getAllClassMetadata();
		for(Iterator<?> m = metadata.values().iterator(); m.hasNext();) {
			SingleTableEntityPersister entity = (SingleTableEntityPersister)m.next();
			String entityName = entity.getName();
			String className = entityDef.getEntityClass().getName();
			if(entityName.equals(className)) {
				String[] columns = entity.getPropertyColumnNames(propertyName);
				if(columns.length>0)
					return columns[0];
				return null;
			}
		}
		return null;
	}
	@Override
	public String getTableFromMetaData(EntityDefinition entityDef) {
		ClassMetadata cmd = getSessionFactory().getClassMetadata(entityDef.getEntityClass());
		if (cmd == null || !Joinable.class.isInstance(cmd))
	        return null;
	    return Joinable.class.cast(cmd).getTableName();		
	}
	@Override
	public Object getByIdForUpdate(EntityDefinition entityDef, Serializable id) {
		try {
			Object object = getHibernateTemplate().get(entityDef.getEntityClass(), id, LockMode.PESSIMISTIC_WRITE);
			clearCache();
			return object;
		} catch(Exception e) {
			throw new DataAccessException(entityDef.getEntityClass(), ACTION_GETBYIDFORUPDATE, e);
		}
	}	
	@Override
	public Object getById(EntityDefinition entityDef, Serializable id) {
		try {
			Object object = getHibernateTemplate().get(entityDef.getEntityClass(), id);
			clearCache();
			return object;
		} catch(Exception e) {
			throw new DataAccessException(entityDef.getEntityClass(), ACTION_GETBYID, e);
		}
	}	
	
	
	@Override
	public Number update(final EntityDefinition entityDef, final Attribute[] attributes, final RestrictionBuilder restriction) {
		try {
			Long recordsUpdated = (getHibernateTemplate().execute(new HibernateCallback<Long>() {
				public Long doInHibernate(Session session)
						throws HibernateException, SQLException {
					DetachedCriteria criteria = getCriteria(entityDef, (restriction==null)?Collections.EMPTY_LIST:restriction.getRestriction());
					ScrollableResults objs = criteria.getExecutableCriteria(session).setCacheMode(CacheMode.IGNORE).scroll(ScrollMode.FORWARD_ONLY);
					long count=0;
					while(objs.next()) {
						Object toUpdate = objs.get(0);
						for(Attribute attribute: attributes) {
							String property = attribute.getName();
							Object value = attribute.getValue();
							try {
								PropertyUtils.setNestedProperty(toUpdate, property, value);
							} catch (Exception e) {
								if(e instanceof RuntimeException)
									throw (RuntimeException)e;
								else
									throw new RuntimeException(e);
							}
						}
					    if ( ++count % CACHE_SIZE == 0 ) {
					        session.flush();
					        session.clear();
					    }					
					}
					return new Long(count);
				}
			}));		
			clearCache();
			return recordsUpdated;
		} catch(Exception e) {
			throw new DataAccessException(null, ACTION_EXECUTEUPDATE, e);
		}
	}
	@Override
	public Number executeUpdate(final String query, final Attribute[] attributes) {
		try {
			Long recordsUpdated = (getHibernateTemplate().execute(new HibernateCallback<Long>() {
				public Long doInHibernate(Session session)
						throws HibernateException, SQLException {
					Query hibernateQuery = session.createQuery(query);
						for(Attribute attribute: attributes) {
							Object value = attribute.getValue();
							if(value instanceof Collection)
								hibernateQuery.setParameterList(attribute.getName(), (Collection<?>)value);
							else if(value instanceof Object[]) 
								hibernateQuery.setParameterList(attribute.getName(), (Object[])value);
							else
								hibernateQuery.setParameter(attribute.getName(), attribute.getValue());
						}
					int update = hibernateQuery.executeUpdate();
					return new Long(update);
				}
			}));		
			clearCache();
			return recordsUpdated;
		} catch(Exception e) {
			if(query!=null&&query.toUpperCase().trim().startsWith("DELETE")) 
				throw new DataAccessException(null, ACTION_EXECUTEDELETE, e);
			else
				throw new DataAccessException(null, ACTION_EXECUTEUPDATE, e);
		}
	}
	@SuppressWarnings("rawtypes")
	@Override
	public Collection<?> executeQuery(final String query, final Attribute[] attributes,
				final Limits limits) {
		try {
			List<?> list = getHibernateTemplate().execute(new HibernateCallback<List>() {
				public List doInHibernate(Session session)
						throws HibernateException, SQLException {
					Query hibernateQuery = session.createQuery(query);
					for(Attribute attribute: attributes) {
						Object value = attribute.getValue();
						String parameter = attribute.getName();
						if(value instanceof Collection)
							hibernateQuery.setParameterList(parameter, (Collection)value);
						else if(value instanceof Object[])
							hibernateQuery.setParameterList(parameter, (Object[])value);
						else
							hibernateQuery.setParameter(parameter, value);
					}
					hibernateQuery.setFirstResult((int)limits.getFirstRecord());				
					if(limits.getNumRecords()>-1)
						hibernateQuery.setMaxResults((int)limits.getNumRecords());
					return hibernateQuery.list();
				}
			});
			clearCache();
			return list!=null?list:EMPTY_ARRAY;
		} catch(Exception e) {
			throw new DataAccessException(null, ACTION_EXECUTEQUERY, e);
		}
	}
	@Override
	public Object getAggregate(EntityDefinition entityDef, Aggregate agg, String propertyName, RestrictionBuilder restriction) {
		if(Aggregate.sum==agg) 
			return getSum(entityDef, propertyName, restriction);
		else if(Aggregate.avg == agg)
			return getAvg(entityDef, propertyName, restriction);
		else if(Aggregate.min ==agg)
			return getMin(entityDef, propertyName, restriction);
		else if(Aggregate.max ==agg)
			return getMax(entityDef, propertyName, restriction);
		else
			throw new RuntimeException("Agregate is not supported");
	}
	@SuppressWarnings("rawtypes")
	private Object getMax(EntityDefinition entityDef, String propertyName, RestrictionBuilder restriction) {
		try {
			DetachedCriteria criteria = getCriteria(entityDef, (restriction==null)?Collections.EMPTY_LIST:restriction.getRestriction());
			criteria = criteria.setProjection(Projections.max(propertyName));
			List f = getHibernateTemplate().findByCriteria(criteria);
			Object result = f.get(0);
			clearCache();
			return result;
		} catch(Exception e) {
			throw new DataAccessException(entityDef.getEntityClass(), ACTION_GETMAX, e);
		}
	}
	@SuppressWarnings("rawtypes")
	private Object getMin(EntityDefinition entityDef, String propertyName, RestrictionBuilder restriction) {
		try {
			DetachedCriteria c = getCriteria(entityDef, (restriction==null)?Collections.EMPTY_LIST:restriction.getRestriction());
			c = c.setProjection(Projections.min(propertyName));
			List f = getHibernateTemplate().findByCriteria(c);
			Object result = f.get(0);
			clearCache();
			return result;
		} catch(Exception e) {
			throw new DataAccessException(entityDef.getEntityClass(), ACTION_GETMIN, e);
		}
	}
	@SuppressWarnings("rawtypes")
	private Object getAvg(EntityDefinition entityDef, String propertyName, RestrictionBuilder restriction) {
		try {
			DetachedCriteria c = getCriteria(entityDef, (restriction==null)?Collections.EMPTY_LIST:restriction.getRestriction());
			c = c.setProjection(Projections.avg(propertyName));
			List f = getHibernateTemplate().findByCriteria(c);
			Object result = f.get(0);
			clearCache();
			return result;
		} catch(Exception e) {
			throw new DataAccessException(entityDef.getEntityClass(), ACTION_GETAVG, e);
		}
	}
	@SuppressWarnings("rawtypes")
	private Object getSum(EntityDefinition entityDef, String propertyName, RestrictionBuilder restriction) {
		try {
			DetachedCriteria c = getCriteria(entityDef, (restriction==null)?Collections.EMPTY_LIST:restriction.getRestriction());
			c = c.setProjection(Projections.sum(propertyName));
			List f = getHibernateTemplate().findByCriteria(c);
			Object result = f.get(0);
			clearCache();
			return result;
		} catch(Exception e) {
			throw new DataAccessException(entityDef.getEntityClass(), ACTION_GETSUM, e);
		}
	}
	@Override
	public Number count(EntityDefinition entityDef, String projection, RestrictionBuilder restriction) {
		try {
			DetachedCriteria criteria = getCriteria(entityDef, (restriction==null)?Collections.EMPTY_LIST:restriction.getRestriction());
			String[] projectionProps = getProjectionProps(projection);
			projection = projectionProps[0];
			String projectionType = projectionProps[1];
			if(PROJECTION_TYPE_PROPERTY.equals(projectionType))
				criteria = criteria.setProjection(Projections.count(projection));
			else if(PROJECTION_TYPE_DISTINCTPROPERTY.equals(projectionType))
				criteria = criteria.setProjection(Projections.countDistinct(projection));
			else
				throw new RuntimeException("projection type is not supported");
			List f = getHibernateTemplate().findByCriteria(criteria);
			long size = 0;
			if(f!=null&&f.size()>0)
				size = ((Long)f.get(0)).longValue();
			clearCache();
			return size;
		} catch(Exception e) {
			throw new DataAccessException(entityDef.getEntityClass(), ACTION_COUNT, e);
		}
	}
	@SuppressWarnings("rawtypes")
	@Override
	public Number count(EntityDefinition entityDef, RestrictionBuilder restriction) {
		try {
			DetachedCriteria c = getCriteria(entityDef, (restriction==null)?Collections.EMPTY_LIST:restriction.getRestriction());
			c = c.setProjection(Projections.rowCount());
			List f = getHibernateTemplate().findByCriteria(c);
			long size = 0;
			if(f!=null&&f.size()>0)
				size = ((Long)f.get(0)).longValue();
			clearCache();
			return size;
		} catch(Exception e) {
			throw new DataAccessException(entityDef.getEntityClass(), ACTION_COUNT, e);
		}
	}
	private String getNextToken(Scanner scanner) {
		if(scanner.hasNext())
			return scanner.next();
		else
			return null;
	}	
	private DetachedCriteria getCriteria(EntityDefinition entityDef, Collection<?> where) {
		DetachedCriteria c = null;
		EntityProperties entityProps = entityDef.getEntityProperties();
		String rootAlias = entityProps.getRootAlias();
		Class<?> clazz = entityDef.getEntityClass();
		if(rootAlias==null||rootAlias.equals(""))
			c = DetachedCriteria.forClass(clazz);
		else
			c = DetachedCriteria.forClass(clazz, rootAlias);
		c.setResultTransformer(DetachedCriteria.DISTINCT_ROOT_ENTITY);
		
		String alias = null;
		String association = null;
		String token = null;
		for(String associationPath: entityProps.getAssociationPaths()) {
			Pattern pattern = Pattern.compile(ASSOCIATION_PATH_EXPRESSION);
			Matcher matcher = pattern.matcher(associationPath);
			if(!matcher.matches())
				throw new RuntimeException("association path is incorrect, syntax can be: "+ASSOCIATION_PATH_EXPRESSION);
			Scanner scanner = new Scanner(associationPath);
			association = getNextToken(scanner);
			alias = null;
			token = getNextToken(scanner);
			if("as".equals(token)) {
				alias = getNextToken(scanner);
				token = getNextToken(scanner);
			}
			int joinType = CriteriaSpecification.LEFT_JOIN;
			if("innerJoin".equals(token)) {
				joinType = CriteriaSpecification.INNER_JOIN;
				token = getNextToken(scanner);
			}
			else if("fullJoin".equals(token)) {
				joinType = CriteriaSpecification.FULL_JOIN;
				token = getNextToken(scanner);
			}
			else if("leftJoin".equals(token)) {
				joinType = CriteriaSpecification.LEFT_JOIN;
				token = getNextToken(scanner);
			}
			if(alias==null)
				c.createCriteria(association, joinType);
			else
				c.createCriteria(association, alias, joinType);	
		}
		if (!where.isEmpty()) {
			Conjunction conjunction = Restrictions.conjunction();
			for(Object o: where) 
				addRestriction(conjunction, o);
			c.add(conjunction);
		}
		return c;
	}	
	private void addRestriction(Junction junction, Object restriction) {
		if(restriction instanceof Collection) {
			Collection<?> _collection = (Collection<?>)restriction;
			Disjunction disjunction = Restrictions.disjunction();
			for(Object _o: _collection) 
				addRestriction(disjunction, _o);
			junction.add(disjunction);
		} else if(restriction instanceof Criterion)
			junction.add((Criterion)restriction);
		else
			throw new RuntimeException("restriction is not supported");
	}
	@Override
	public Collection<?> getByAttribute(EntityDefinition entityDef, RestrictionAttribute attribute,
			SOrder[] orderBy, Limits limits) {
		RestrictionBuilder restrictionBuilder = createRestrictionBuilder();
		restrictionBuilder.addCondition(attribute, BooleanOperator.AND);
		return getByRestriction(entityDef, restrictionBuilder, orderBy, limits);
	}
	public Collection<?> getAll(EntityDefinition entityDef, SOrder[] orderBy) {
		return getByRestriction(entityDef, null, orderBy, Limits.allRecs());
	}	
	@SuppressWarnings("rawtypes")
	@Override
	public Collection<?> getByRestrictionAsMaps(EntityDefinition entityDef,
			RestrictionBuilder restriction, SOrder[] orderBy, Limits limits) {
		try {
			DetachedCriteria c = getCriteria(entityDef, (restriction==null)?Collections.EMPTY_LIST:restriction.getRestriction());
			c = c.setResultTransformer(Criteria.ALIAS_TO_ENTITY_MAP);
			if (orderBy != null) {
				for (SOrder e: orderBy) {
					if (DirectionOrder.dsc == e.getDirection()) 
						c = c.addOrder(Order.desc((String)e.getProperty()));
					else 
						c = c.addOrder(Order.asc((String)e.getProperty()));
				}
			}			
			List list = getHibernateTemplate().findByCriteria(c, (int)limits.getFirstRecord(), (int)limits.getNumRecords());
			clearCache();
			return (list!=null)?list:EMPTY_ARRAY;
		} catch(Exception e) {
			throw new DataAccessException(entityDef.getEntityClass(), ACTION_GETBYRESTRICTIONASMAP, e);
		}
	}
	private String[] getProjectionProps(String projection) {
		String token;
		String projectionType = PROJECTION_TYPE_PROPERTY;
		Pattern pattern = Pattern.compile(PROJECTION_EXPRESSION);
		Matcher matcher = pattern.matcher(projection);
		if(!matcher.matches())
			throw new RuntimeException("association path is incorrect, syntax can be: "+ASSOCIATION_PATH_EXPRESSION);
		Scanner scanner = new Scanner(projection);
		token = getNextToken(scanner);
		if(token==null||"".equals(token))
			throw new RuntimeException("projection can't be empty");
		projection = token;
		token = getNextToken(scanner);
		if(token!=null && !"".equals(token))
			projectionType = token;
		if(!(PROJECTION_TYPE_PROPERTY.equals(projectionType)
		|| PROJECTION_TYPE_DISTINCTPROPERTY.equals(projectionType)
		|| PROJECTION_TYPE_SQL.equals(projectionType)))
			throw new RuntimeException("projection type is not supported");
		return new String[]{projection, projectionType};
	}
	protected DetachedCriteria addProjection(DetachedCriteria criteria, String projection) {
		return addProjections(criteria, new String[]{projection});
	}
	protected DetachedCriteria addProjections(DetachedCriteria criteria, String[] projections) {
		ProjectionList list = Projections.projectionList();
		for(String projection: projections) {
			String[] projectionProps = getProjectionProps(projection);
			projection = projectionProps[0];
			String projectionType = projectionProps[1]; 
			if(PROJECTION_TYPE_PROPERTY.equals(projectionType))
				list.add(Projections.property(projection));
			else if(PROJECTION_TYPE_DISTINCTPROPERTY.equals(projectionType))
				list.add(Projections.distinct(Projections.property(projection)));
			else if(PROJECTION_TYPE_SQL.equals(projectionType))
				list.add(Projections.sqlProjection(projection, null, null));
			else
				throw new RuntimeException("projection type is not supported");
		}
		return criteria.setProjection(list);
	}
	@Override
	public Object createSubquery(EntityDefinition entityDef, String[] projections, RestrictionBuilder restriction) {
		DetachedCriteria criteria = getCriteria(entityDef, (restriction==null)?Collections.EMPTY_LIST:restriction.getRestriction());
		criteria = addProjections(criteria, projections);
		return criteria;
	}
	protected DetachedCriteria addOrderBy(DetachedCriteria criteria, SOrder[] orderBy) {
		if (orderBy != null) {
			for (SOrder e: orderBy) {
				if (DirectionOrder.dsc == e.getDirection()) 
					criteria = criteria.addOrder(Order.desc((String)e.getProperty()));
				else 
					criteria = criteria.addOrder(Order.asc((String)e.getProperty()));
			}
		}			
		return criteria;
	}
	@Override
	public Collection<?> getByRestriction(EntityDefinition entityDef, String[] projections, RestrictionBuilder restriction, SOrder[] orderBy, Limits limits) {
		try {
			DetachedCriteria criteria = getCriteria(entityDef, (restriction==null)?Collections.EMPTY_LIST:restriction.getRestriction());
			criteria = addProjections(criteria, projections);
			criteria = addOrderBy(criteria, orderBy);
			List<?> list = getHibernateTemplate().findByCriteria(criteria, (int)limits.getFirstRecord(), (int)limits.getNumRecords());
			clearCache();
			return (list!=null)?list:EMPTY_ARRAY;
		} catch(Exception e) {
			throw new DataAccessException(entityDef.getEntityClass(), ACTION_GETBYRESTRICTION, e);
		}
	}
	@Override
	public Collection<?> getByRestriction(EntityDefinition entityDef,
			RestrictionBuilder restriction, SOrder[] orderBy, Limits limits) {
		try {
			DetachedCriteria criteria = getCriteria(entityDef, (restriction==null)?Collections.EMPTY_LIST:restriction.getRestriction());
			criteria = addOrderBy(criteria, orderBy);
			List<?> list = getHibernateTemplate().findByCriteria(criteria, (int)limits.getFirstRecord(), (int)limits.getNumRecords());
			clearCache();
			return (list!=null)?list:EMPTY_ARRAY;
		} catch(Exception e) {
			throw new DataAccessException(entityDef.getEntityClass(), ACTION_GETBYRESTRICTION, e);
		}
	}
	@Override
	public boolean existsWithId(EntityDefinition entityDef, Serializable id) {
		try {
			boolean exists = (getHibernateTemplate().get(entityDef.getEntityClass(), id) != null);
			clearCache();
			return exists;
		} catch(Exception e) {
			throw new DataAccessException(entityDef.getEntityClass(), ACTION_GETBYID, e);
		}
	}
	@Override
	public boolean existsWithRestriction(EntityDefinition entityDef, RestrictionBuilder restriction) {
		Collection<?> result = getByRestriction(entityDef, restriction, null, Limits.oneRec());
		clearCache();
		return result.size()>0;
	}
	public RestrictionBuilder createRestrictionBuilder() {
		return new HibernateRestrictionBuilder();
	}
	@Override
	public Blob createBlob( Object... args) {
		if(args.length<=0)
			throw new RuntimeException("args to create blob can't be null");
		/*
		Blob blob = (Blob) getHibernateTemplate().execute(new HibernateCallback() {
			public Object doInHibernate(Session session)
					throws HibernateException, SQLException {
					return _createBlob(session, args);
			}
		});
		*/
		Blob blob = _createBlob(null, args);
		clearCache();
		return blob;
	}
	//TODO: quitar cuando hibernate quite el bug que ocurre cuando se obtiene la sessi�n 
	private Blob _createBlob(Session session,  Object... args) {
		Blob blob = null;
		if(args[0] instanceof byte[]) {
			byte[] data = (byte[])args[0];
			if(session==null)
				blob = Hibernate.createBlob(data);
			else
				blob = Hibernate.createBlob(data, session);
		} else if(args[0] instanceof InputStream) {
			if(args.length<=1)
				throw new RuntimeException("args to create blob with inpustream is incorrect");
			Long length = (Long)args[1];
			InputStream data = (InputStream)args[0];
			if(session==null)
				blob = Hibernate.createBlob(data, length);
			else
				blob = Hibernate.createBlob(data, length, session);
		}
		return blob;		
	}
	@Override
	public void setTransactionManager(Object transactionManager) {
		transactionTemplate = new TransactionTemplate((PlatformTransactionManager)transactionManager);
	}
	@Override
	public Object getTransactionTemplate() {
		return transactionTemplate;
	}
	@Override
	public Object executeInTransaction(final Transaction transaction) {
		if(transactionTemplate == null) throw new RuntimeException("transaction tempalte can't be null to execute a transaction");
		if(transaction==null)throw new RuntimeException("database operation can't be null");
		Object result = transactionTemplate.execute(new TransactionCallback<Object>() {
			@Override
			public Object doInTransaction(TransactionStatus status) {
				try {
					return transaction.execute(status);
				} catch(Exception e) {
					if(e instanceof RuntimeException)
						throw (RuntimeException)e;
					else
						throw new RuntimeException(e);
				}
			}
		});
		return result;
	}
	@Override
	public Object execute(final DBOperation operation) {
		if(operation==null)throw new RuntimeException("database operation can't be null");
		Object result = getHibernateTemplate().execute(new HibernateCallback<Object>() {
			public Object doInHibernate(Session session)
					throws HibernateException, SQLException {
				Connection connection = SessionFactoryUtils.getDataSource(session.getSessionFactory()).getConnection();
				Object result = null;
				try {
					result = operation.execute(connection);
					connection.commit();
				} catch(Exception e) {
					try {
						connection.rollback();
					} catch(Exception e2) {/*IGNORE EXCEPTION*/}
					if(e instanceof RuntimeException)
						throw (RuntimeException)e;
					else
						throw new RuntimeException(e);
				} finally {
					try {
						if(connection!=null)
							connection.close();
					} catch(Exception _e) {/*IGNORE EXCEPTION*/}
				}
				return result;
			}
		});
		return result;
	}
}
